/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package webserver;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.Timestamp;
import org.apache.commons.lang.StringUtils;

/**
 * CachItem represents all needed Values for a single entry in the Cache
 * 
 */
public class CacheItem {

	// Static Declarations
	/**
	 * The default time (ms) a cache entry will stay in cache
	 */
	public static final long DEFAULT_CACHE_TIME = 60000;
	/**
	 * The maximum time (ms) a cache entry can stay in cache (Max is 86.400.000 = 1 day)
	 */
	public static final long MAX_CACHE_TIME = 24 * 60 * 60 * 1000;
	/**
	 * Multiplier for the factor AccessCounts
	 */
	public static final double ACCESS_COUNT_MULTIPLIER = 0.08;
	/**
	 * The upper border AccessCounts can influence cache time (ms) (15.000.000 > 4h)
	 */
	public static final long ACCESS_COUNT_BORDER = 15000000;
	/**
	 * The exponent factor to use with the e-function
	 */
	public static final double ACCESS_COUNT_EXP = -0.025;
	/**
	 * The start value for the function
	 */
	public static final long ACCESS_COUNT_REDUCE = 200000;
	/**
	 * The upper border FileSize can influence cach time (ms) (7.200.000 = 2h)
	 */
	public static final long FILE_SIZE_BORDER = 7200000;
	/**
	 * The exponent factor to use with the e-function
	 */
	public static final double FILE_SIZE_EXP = 0.000025;
	/**
	 * Multiplier for the factor FileSize
	 */
	public static final double FILE_SIZE_MULTIPLIER = -0.01;
	/**
	 * The start value for the function
	 */
	public static final long FILE_SIZE_REDUCE = 3600000;
	/**
	 * Threshold to use with Levensthein function to determine mistyped words
	 */
	public static final int LEVENSTHEIN_THRESHOLD = 5;

	/**
	 * Enum representing the file extensions to get the mime types
	 */
	public enum mimetypes {

		JPG, JPE, JPEG, GIF, ICO, PNG, HTM, HTML, SHTML, PDF, RTF, XML, ZIP, WAV, TIF, TIFF, TXT, MPG, MPE, MPEG, AVI
	};
	// Variables
	private final String path;
	private byte data[];
	private int accessCount;
	private int fileSize;
	private Timestamp invalidDate;
	private Timestamp firstAccess;
	private Timestamp lastModified;
	private long lifetime;

	/**
	 * CacheItem Overloaded Constructor
	 * @param absPath - The path to the file
	 */
	public CacheItem(String absPath) {
		this(new File(absPath));
	}

	/**
	 * CacheItem Default Constructor
	 * @param file - The file which should be read
	 */
	public CacheItem(File file) {
		// Initialize
		this.path = file.getAbsolutePath();
		this.lastModified = null;
		this.accessCount = 0;
		this.firstAccess = new Timestamp(new java.util.Date().getTime());
		this.invalidDate = new Timestamp(firstAccess.getTime() + DEFAULT_CACHE_TIME);

		// Check file size
		if (!file.isDirectory() && file.length() > Integer.MAX_VALUE) {
			throw new IllegalArgumentException("The file is bigger than MAX_INTEGER: " + file.getAbsolutePath());
		}

		this.readFile(file);
	}

	/**
	 * This function tries to guess the content type of the file.
	 * It only uses the file extension to check for known and supported types
	 * @return String - mime type representation (like: text/html)
	 */
	public String getContentType() {
		String extension = "";
		int dotPos = this.path.lastIndexOf(".") + 1;
		if (dotPos != 0) {
			extension = this.path.substring(dotPos).toUpperCase();
		}

		try {
			switch (mimetypes.valueOf(extension)) {
				case JPEG:
				case JPG:
				case JPE:
					return "image/jpeg";

				case GIF:
					return "image/gif";

				case PNG:
					return "image/png";

				case ICO:
					return "image/x-icon";

				case HTML:
				case HTM:
				case SHTML:
					return "text/html";

				case PDF:
					return "application/pdf";

				case RTF:
					return "application/rtf";

				case XML:
					return "application/xml";

				case ZIP:
					return "application/zip";

				case WAV:
					return "audio/x-wav";

				case TIF:
				case TIFF:
					return "image/tiff";

				case TXT:
					return "text/plain";

				case MPEG:
				case MPG:
				case MPE:
					return "video/mpeg";

				case AVI:
					return "video/x-msvideo";
			}
		} catch (IllegalArgumentException ia) {
			Logger.writeToLog("Cannot determine MIME-Type for extension: " + extension);
		}

		return "text/html";
	}

	/**
	 * Read the contents of the file
	 * @param file - The file to read
	 */
	private void readFile(File file) {
		this.lastModified = new Timestamp(file.lastModified());

		// Check if the item is a directory and get the dir contents
		if (file.isDirectory() && file.exists()) {
			//Check for index.html/.htm
			File indexHtml = new File(file.getAbsolutePath() + "/index.html");
			File indexHtm = new File(file.getAbsoluteFile() + "/index.htm");

			File index = indexHtml.exists() ? indexHtml : indexHtm;
			if (index.exists()) {
				this.fileSize = (int) index.length();
				this.data = new byte[this.fileSize];

				// Read the file contents
				try {
					FileInputStream stream = new FileInputStream(index);
					BufferedInputStream reader = new BufferedInputStream(stream);

					// Read the whole file
					int pos = 0;
					do {
						pos += reader.read(this.data, pos, this.fileSize - pos);
					} while (pos != this.fileSize);
				} catch (IOException ex) {
					Logger.writeToLog("Error reading file: " + index.getAbsolutePath() + ": " + ex.toString());
				}
				return;
			}

			String[] contents = file.list();
			StringBuffer ret = new StringBuffer("");

			// Loop through folder contents
			String homeDirectory = AppProperties.getInstance().getHomeDirectory();
			for (int i = 0; i < contents.length; i++) {
				String temp = file.getAbsolutePath();
				temp = temp.replace(homeDirectory, "");
				temp = temp.replace("\\", "/");
				temp = temp.substring(temp.indexOf("/", 1) + 1);
				temp += "/" + contents[i];
				if (ret.length() != 0) {
					ret.append("<br />\n");
				}
				ret.append("<li><a href=\"http://localhost:");
				ret.append(AppProperties.getInstance().getPort());
				ret.append("/");
				ret.append(temp);//contents[i] + dir);
				ret.append("\">");
//				ret.append(temp);
//				ret.append("/");
				ret.append(contents[i]);
				ret.append("</a></li>\n");
			}

			// Append HTML standard header elements
			StringBuffer complete = new StringBuffer("<html>\n<head>\n<title>Directory</title>\n</head>\n<body>\n");
			complete.append("<h2>Contents of Directory " + file.getName() + ":</h2>\n<br />\n<ul>\n");
			complete.append(ret);
			complete.append("</ul>\n</body>\n</html>");

			// Write to Data
			this.data = new byte[complete.length()];
			this.data = complete.toString().getBytes();
			this.fileSize = this.data.length;

			// The given File is a file (not dir)
		} else {
			this.fileSize = (int) file.length();
			this.data = new byte[this.fileSize];

			// Read the file contents
			try {
				FileInputStream stream = new FileInputStream(file);
				BufferedInputStream reader = new BufferedInputStream(stream);

				// Read the whole file
				int pos = 0;
				do {
					pos += reader.read(this.data, pos, this.fileSize - pos);
				} while (pos != this.fileSize);

			} catch (FileNotFoundException fnfe) {
				Logger.writeToLog("The file could not be found: " + file.getAbsolutePath() + ": " + fnfe.toString());

				// Check for similar files
				String[] contents = file.getParentFile().list();
				StringBuffer ret = new StringBuffer("");

				// Loop through parent folder contents
				for (int i = 0; i < contents.length; i++) {
					// Check Levensthein Distance
					int lDist = StringUtils.getLevenshteinDistance(file.getName(), contents[i]);
					if (lDist <= LEVENSTHEIN_THRESHOLD) {
						if (ret.length() != 0) {
							ret.append("<br />\n");
						}
						ret.append("<li><a href=\"./");
						ret.append(contents[i]);
						ret.append("\">");
						ret.append(contents[i]);
						ret.append("</a></li>\n");
					}
				}

				if (ret.length() != 0) {
					// Add HTML standard header elements
					StringBuffer complete = new StringBuffer("<html>\n<head>\n<title>No direct match found</title>\n</head>\n<body>\n");
					complete.append("<h2>Did you mean ...</h2>\n<br />\n<ul>\n");
					complete.append(ret);
					complete.append("</ul>\n</body>\n</html>");

					this.data = new byte[complete.length()];
					this.data = complete.toString().getBytes();
					this.fileSize = this.data.length;
				}
			} catch (IOException ioe) {
				Logger.writeToLog("Error reading file: " + file.getAbsolutePath() + ": " + ioe.toString());
			}
		}
	}

	/**
	 * Check whether the CacheItem is still valid
	 * @return boolean - Represents the validness of this Item
	 */
	public synchronized boolean isValid() {
		Timestamp current = new Timestamp(new java.util.Date().getTime());
		return current.before(this.invalidDate);
	}

	/**
	 * Generate the time this item will be hold in Cache
	 */
	private synchronized void generateLifetime() {
		this.lifetime = DEFAULT_CACHE_TIME;
		double access = ACCESS_COUNT_MULTIPLIER * (ACCESS_COUNT_BORDER - (ACCESS_COUNT_BORDER - ACCESS_COUNT_REDUCE) * Math.pow(Math.E, ACCESS_COUNT_EXP * this.accessCount));
		this.lifetime += access;
		double size = FILE_SIZE_MULTIPLIER * (FILE_SIZE_BORDER - (FILE_SIZE_BORDER - FILE_SIZE_REDUCE) * Math.pow(Math.E, FILE_SIZE_EXP * (this.fileSize / 1024)));
		this.lifetime += size;

		if (this.lifetime > MAX_CACHE_TIME) {
			this.lifetime = MAX_CACHE_TIME;
		}
		if (this.lifetime < DEFAULT_CACHE_TIME) {
			this.lifetime = DEFAULT_CACHE_TIME;
		}

		this.invalidDate = new Timestamp(this.firstAccess.getTime() + this.lifetime);
		//System.out.println("AccessCount: " + access + " FileSize:" + size + " Lifetime: " + this.lifetime);
	}

	/**
	 * Get the Data of this CacheItem
	 * @return byte[] - the data
	 */
	public synchronized byte[] getData() {
		// Check whether the entry is still valid
		if (!this.isValid()) {
			// Reload file
			this.refresh();
		}

		// Deliver file data and generate new Lifetime
		this.accessCount++;
		this.generateLifetime();
		return this.data;
	}

	/**
	 * Refresh the cache item with the latest file contents
	 */
	public synchronized void refresh() {
		this.accessCount = 1;
		this.readFile(new File(this.path));
	}

	/**
	 * Get the absolut file path to the cached file
	 * @return String
	 */
	public String getAbsPath() {
		return this.path;
	}

	/**
	 * Get the Timestamp when the resource has been modified
	 * @return Timestamp
	 */
	public Timestamp lastModified() {
		return this.lastModified;
	}

	/**
	 * Return the file size of the cached file
	 * @return int
	 */
	public int getFileSize() {
		return this.fileSize;
	}
}
